// Tools for serializing a backend into a JSON-stringifiable object format,
// and for deserializing this format and loading its contained data into an
// existing backend instance.
//
// Serialized data includes the list of queue players and each player's state
// (queued items, playback position, etc).
//
// Serialized backend data can be used for a variety of purposes, such as
// writing the data to a file and saving it for later use, or transferring
// it over an internet connection to synchronize playback with a friend.
// (The code in socket.js exists to automate this process, as well as to
// provide a link so that changes to the queue or playback are synchronized
// in real-time.)
//
// TODO: Changes might be necessary all throughout the program to support
// having any number of objects refer to "the same track", as will likely be
// the case when restoring from a serialized backend. One way to handle this
// would be to (perhaps through the existing record store code) keep a handle
// on each of "the same track", which would be accessed by something like a
// serialized ID (ala symbols), or maybe just the track name / source URL.

'use strict'

import {
  isGroup,
  isTrack,
  findItemObject,
  flattenGrouplike,
  getFlatGroupList,
  getFlatTrackList,
  getItemPath
} from './playlist-utils.js'

const referenceDataSymbol = Symbol('Restored reference data')

function getPlayerInfo(queuePlayer) {
  const { player } = queuePlayer
  return {
    time: queuePlayer.time,
    isLooping: player.isLooping,
    isPaused: player.isPaused,
    volume: player.volume
  }
}

export function saveBackend(backend) {
  return {
    queuePlayers: backend.queuePlayers.map(QP => ({
      id: QP.id,
      playingTrack: saveItemReference(QP.playingTrack),
      queuedTracks: QP.queueGrouplike.items.map(saveItemReference),
      pauseNextTrack: QP.pauseNextTrack,
      playerInfo: getPlayerInfo(QP)
    }))
  }
}

export async function restoreBackend(backend, data) {
  // console.log('restoring backend:', data)

  if (data.queuePlayers) {
    if (data.queuePlayers.length === 0) {
      return
    }

    for (const qpData of data.queuePlayers) {
      const QP = await backend.addQueuePlayer()
      QP[referenceDataSymbol] = qpData

      QP.id = qpData.id

      QP.queueGrouplike.items = qpData.queuedTracks.map(refData => restoreNewItem(refData))

      QP.player.setVolume(qpData.playerInfo.volume)
      QP.player.setLoop(qpData.playerInfo.isLooping)

      QP.on('playing', () => {
        QP[referenceDataSymbol].playingTrack = null
        QP[referenceDataSymbol].playerInfo = null
      })
    }

    // We remove the old queue players after the new ones have been added,
    // because the backend won't let us ever have less than one queue player
    // at a time.
    while (backend.queuePlayers.length !== data.queuePlayers.length) {
      backend.removeQueuePlayer(backend.queuePlayers[0])
    }
  }
}

async function restorePlayingTrack(queuePlayer, playedTrack, playerInfo) {
  const QP = queuePlayer
  await QP.stopPlaying()
  QP.play(playedTrack, playerInfo.time || 0, playerInfo.isPaused)
}

export function updateRestoredTracksUsingPlaylists(backend, playlists) {
  // Utility function to restore the "identities" of tracks (i.e. which objects
  // they are represented by) queued or playing in the provided backend,
  // pulling possible track identities from the provided playlists.
  //
  // How well provided tracks resemble the ones existing in the backend (which
  // have not already been replaced by an existing track) is calculated with
  // the algorithm implemented in findItemObject, combining all provided
  // playlists (simply putting them all in a group) to allow the algorithm to
  // choose from all playlists equally at once.
  //
  // This function should be called after restoring a playlist and whenever
  // a new source playlist is added (a new tab opened, etc).
  //
  // TODO: Though this helps to combat issues with restoring track identities
  // when restoring from a saved backend, it could be expanded to restore from
  // closed sources as well (reference data would have to be automatically
  // saved on the tracks independently of save/restore in order to support
  // this sort of functionality). Note this would still face difficulties with
  // opening two identical playlists (i.e. the same playlist twice), since then
  // identities would be equally correctly picked from either source; this is
  // an inevitable issue with the way identities are resolved, but could be
  // lessened in the UI by simply opening a new view (rather than a whole new
  // load, with new track identities) when a playlist is opened twice at once.

  const possibleChoices = getFlatTrackList({items: playlists})

  for (const QP of backend.queuePlayers) {
    let playingDataToRestore

    const qpData = (QP[referenceDataSymbol] || {})
    const waitingTrackData = qpData.playingTrack
    if (waitingTrackData) {
      playingDataToRestore = waitingTrackData
    } else if (QP.playingTrack) {
      playingDataToRestore = QP.playingTrack[referenceDataSymbol]
    }

    if (playingDataToRestore) {
      const found = findItemObject(playingDataToRestore, possibleChoices)
      if (found) {
        restorePlayingTrack(QP, found, qpData.playerInfo || getPlayerInfo(QP))
      }
    }

    QP.queueGrouplike.items = QP.queueGrouplike.items.map(track => {
      const refData = track[referenceDataSymbol]
      if (!refData) {
        return track
      }

      return findItemObject(refData, possibleChoices) || track
    })

    QP.emit('queue updated')
  }
}

export function saveItemReference(item) {
  // Utility function to generate reference data for a track or grouplike,
  // according to the format taken by findItemObject.

  if (isTrack(item)) {
    return {
      name: item.name,
      path: getItemPath(item).slice(0, -1).map(group => group.name),
      downloaderArg: item.downloaderArg
    }
  } else if (isGroup(item)) {
    return {
      name: item.name,
      path: getItemPath(item).slice(0, -1).map(group => group.name),
      items: item.items.map(saveItemReference)
    }
  } else if (item) {
    return item
  } else {
    return null
  }
}

export function restoreNewItem(referenceData, playlists) {
  // Utility function to restore a new item. If you're restoring tracks
  // already present in a backend, use the specific function for that,
  // updateRestoredTracksUsingPlaylists.
  //
  // This function takes a playlists array like the function for restoring
  // tracks in a backend, but in this function, it's optional: if not provided,
  // it will simply skip searching for a resembling track and return a new
  // track object right away.

  let found
  if (playlists) {
    let possibleChoices
    if (referenceData.downloaderArg) {
      possibleChoices = getFlatTrackList({items: playlists})
    } else if (referenceData.items) {
      possibleChoices = getFlatGroupList({items: playlists})
    }
    if (possibleChoices) {
      found = findItemObject(referenceData, possibleChoices)
    }
  }

  if (found) {
    return found
  } else if (referenceData.downloaderArg) {
    return {
      [referenceDataSymbol]: referenceData,
      name: referenceData.name,
      downloaderArg: referenceData.downloaderArg
    }
  } else if (referenceData.items) {
    return {
      [referenceDataSymbol]: referenceData,
      name: referenceData.name,
      items: referenceData.items.map(item => restoreNewItem(item, playlists))
    }
  } else {
    return {
      [referenceDataSymbol]: referenceData,
      name: referenceData.name
    }
  }
}

export function getWaitingTrackData(queuePlayer) {
  // Utility function to get reference data for the track which is currently
  // waiting to be played, once a resembling track is found. This should only
  // be used to reflect that data in the user interface.

  return (queuePlayer[referenceDataSymbol] || {}).playingTrack
}
